/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package TimeRecording.business;

import TimeRecording.business.calcs.DailyAllocation;
import TimeRecording.business.calcs.HolidayAllowance;
import TimeRecording.business.calcs.PatternsSummary;
import TimeRecording.enums.HolidayAdjustmentStatus;
import TimeRecording.enums.HolidayRequestStatus;
import TimeRecording.enums.PartOfDay;
import TimeRecording.tables.Employee;
import TimeRecording.tables.HolidayAdjustment;
import TimeRecording.tables.HolidayRequest;
import TimeRecording.tables.WorkingSummary;
import java.util.Collection;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;

/**
 * @author david
 */
public class HolidayCalculations {

    // The figures below are based on a five day working week over an entire
    // year; if someone works fewer days or only a part of the year then this
    // figure will drop.  Similarly, although it would likely never happen, if
    // someone were contracted to work seven days a week than these numbers
    // would rise.
    
    private final static double WEEKS_PER_YEAR = 5.6;
    private final static double DAYS_PER_YEAR = WEEKS_PER_YEAR * 5;

    private final static String EXPLANATION =
            "Allowance = %1$.2f days @ %2$.2f hours per day = %3$.2f hours total";

    private PeriodCalculations periodCalcs = new PeriodCalculations();

    public static double calculateHolidayEarned(double hours) {
        return hours * WEEKS_PER_YEAR / 53;
    }

    public HolidayCalculations() {
        super();
    }

    // <editor-fold defaultstate="collapsed" desc="Static convenience methods">

    public static String getAnnualAllowanceTip(
            double daysOfLeave, double hoursOfLeave) {
        
        double hoursPerDay = hoursOfLeave / daysOfLeave;
        
        return new Formatter().format(EXPLANATION,
                daysOfLeave, hoursPerDay, hoursOfLeave).toString();

    }

    public static HolidayAllowance calculateAnnualAllowance(
            PatternsSummary patternsSummary) {

        // What percentage of the year is this person contracted for?
        double percentageOfYear = patternsSummary.getEmployedDays() / 365;

        // What percentage of the week is this person contracted for?
        double percentageOfWeek =
                patternsSummary.getWorkingDays() / 
                patternsSummary.getPotentialWorkingDays();

        // Pro-rata their leave based on this percentage
        // and round it to the nearest half day
        double daysOfLeave = Math.round(
                DAYS_PER_YEAR * percentageOfYear * percentageOfWeek * 2.0) / 2.0;

        // What's the mean number of hours worked per day (rounded to 5 minutes)?

        double hoursPerDay = Math.round(
                patternsSummary.getWorkingHours() * 12.0 /
                (double)(patternsSummary.getWorkingDays())) / 12.0;

        double hoursOfLeave = hoursPerDay * daysOfLeave;

        String explanation = new Formatter().format(EXPLANATION,
                daysOfLeave, hoursPerDay, hoursOfLeave).toString();

        return new HolidayAllowance(daysOfLeave, hoursPerDay, explanation);
    }

    // </editor-fold>

    /**
     * Returns the part of the day that is booked as an approved holiday on this
     * date.
     * 
     * @param employee
     * @param date
     * @param mustBeApproved
     * @return
     */

    public static PartOfDay isOnHoliday(Employee employee,
            Date date, boolean mustBeApproved) {

        PartOfDay partOfDayOnHoliday = null;

        for (HolidayRequest request : employee.getHolidayRequests()) {

            if (mustBeApproved && !request.isApproved()) {
                continue;
            }

            PartOfDay partOfDay = DaisyChainUtilities.getDayPart(
                    request.getFirstDayOfHoliday(),
                    request.getLastDayOfHoliday(),
                    request.getFirstDayPart(),
                    request.getLastDayPart(),
                    date);

            // Any holiday on that day?
            if (partOfDay != null) {

                // If we've already encountered a holiday for that day then it
                // can only mean that two separate holiday requests have been
                // entered: onc for AM and one for PM (probbaly will never
                // happen but just in case...)

                if (partOfDayOnHoliday == null) {
                    partOfDayOnHoliday = partOfDay;
                } else {
                    partOfDayOnHoliday = PartOfDay.ALL_DAY;
                }
            }
        }

        return partOfDayOnHoliday;
    }
    
    public static boolean validateRequest(HolidayRequest newRequest) {
        
        Employee newEmployee = newRequest.getEmployeeId();
        Collection<HolidayRequest> requests = newEmployee.getHolidayRequests();

        int newRequestId = 0;
        if (newRequest.getRequestId() != null) {
            newRequestId = newRequest.getRequestId();
        }

        for (HolidayRequest request : requests) {

            // Do not include the holiday itself when checking for a clash
            int requestId = request.getRequestId();
            if (requestId == newRequestId) {
                continue;
            }

            if (request.clashesWith(newRequest)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks to see if the employee has a single holiday booked on this date.
     * 
     * @param employee
     * @param date
     * @return
     */
    public static boolean singleDayBooked(Employee employee,
            Date date, boolean mustBeApproved) {

        for (HolidayRequest request : employee.getHolidayRequests()) {

            if (mustBeApproved && !request.isApproved()) {
                continue;
            }

            if (request.getFirstDayOfHoliday().equals(date) &&
                    request.getLastDayOfHoliday().equals(date) &&
                    request.getFirstDayPart() == PartOfDay.ALL_DAY) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns a single, full-day holiday request for this employee on this date
     * (if the employee has such a single date booked in).
     *
     * @param employee
     * @param date
     * @return
     */
    public static HolidayRequest getPublicHoliday(Employee employee, Date date) {

        for (HolidayRequest request : employee.getHolidayRequests()) {

            if (request.getFirstDayOfHoliday().equals(date) &&
                    request.getLastDayOfHoliday().equals(date) &&
                    request.getFirstDayPart() == PartOfDay.ALL_DAY) {
                
                return request;
            }
        }

        return null;
    }

    /**
     * This method will calculate the holiday spend for the supplied holiday.
     *
     * The class instance is designed to be reusable such that multiple holiday
     * instances can be thrown at it;  if sufficient data is present (a partial
     * "intelligent" lookup is performed when accessing the database) then no
     * back-end access will be performed.
     *
     * @return
     */
    public double getHolidaySpend(EntityManager entityManager,
            HolidayRequest newHoliday, SpendExplanation explanation) {

        // Prepare our explanation (if one supplied)
        if (explanation != null) {
            explanation.start();
        }

        // Take a bullet-proof copy of the the supplied holiday
        // and close off the start and end dates if they're open

        HolidayRequest safeHoliday = new HolidayRequest();
        safeHoliday.copyClipped(newHoliday);

        // If we already have the weekly data for this employee for this
        // date range then we don't need to go back to the database

        periodCalcs.refreshData(entityManager,
                safeHoliday.getEmployeeId(),
                safeHoliday.getFirstDayOfHoliday(),
                safeHoliday.getLastDayOfHoliday());

        periodCalcs.insertHolidayRequest(safeHoliday);

        // Reassess the clocked time, etc.
        periodCalcs.reassessWorkingDays(entityManager);

        double totalCost = 0.0;

        // Work out the holiday cost

        for (DailyAllocation day : periodCalcs.getDailyInformation()) {

            // Skip any days calculated that do not fall in the holiday
            
            if (day.getDate().before(safeHoliday.getFirstDayOfHoliday())) {
                continue;
            }
            if (day.getDate().after(safeHoliday.getLastDayOfHoliday())) {
                continue;
            }

            double cost = day.getTotalHolidayTime();
            
            PartOfDay partOfDayForThisHoliday = PartOfDay.ALL_DAY;
            double divisor = 1.0;
           
            // For our explanation, we'll probably have
            // whole days except possibly at the start
            if (day.getDate().equals(safeHoliday.getFirstDayOfHoliday())) {
                partOfDayForThisHoliday = safeHoliday.getFirstDayPart() ;
            }

            // For our explanation, we'll probably have
            // whole days except possibly at the end
            if (day.getDate().equals(safeHoliday.getLastDayOfHoliday())) {
                partOfDayForThisHoliday = safeHoliday.getLastDayPart() ;
            }

            PartOfDay partOfDayForAllHolidays = PartOfDay.add(
                    day.getHoliday().getPartOfDay(),
                    day.getUnapprovedHoliday().getPartOfDay());

            if (partOfDayForThisHoliday != PartOfDay.ALL_DAY &&
                    partOfDayForAllHolidays == PartOfDay.ALL_DAY){
                divisor = 2.0;
            }
           
            cost = cost / divisor;

            if (explanation != null) {
                explanation.add(day.getDate(), partOfDayForThisHoliday, cost);
            }

            totalCost += cost;
        }
        
        return totalCost;
    }

    /**
     * Reassesses the spend on ALL holiday requests (note that the method
     * currently visits all records but could be tweaked to only visit those
     * that require re-assessment).  The transaction is managed outside of this
     * method.
     *
     * @param  entityManager the entityManager via which database changes are made
     */
    public void reassessHolidays(EntityManager entityManager) {

        Query query = entityManager.createNamedQuery("HolidayRequest.findAll");
        List<HolidayRequest> requests = query.getResultList();

        for (HolidayRequest request : requests) {

            double oldCost = request.getHoursSpent();
            double newCost = getHolidaySpend(entityManager, request, null);

            // Persist any changes
            if (oldCost != newCost) {
                entityManager.merge(request);
            }
        }
    }

    /**
     * Update the holiday running totals for the supplied employee; figures are
     * calculated as follows:
     *
     * Leave Granted - This is the sum of holiday carried forward plus any
     * grants/adjustments made on or before today (but not including those for
     * previous years which will have been archived).
     *
     * Leave Taken - The sum of all processed leave this year plus any portion
     * of approved leave up to and including today.
     *
     * Leave Approved - The sum of all approved leave from tomorrow until
     * the end of the holiday year.
     *
     * Leave Not Yet Approved Taken - The sum of all not-yet approved leave for
     * the current holiday year.
     *
     * Leave Remaining - The leave granted figure minus all holiday requests for
     * the holiday year, regardless of their status (they may not even have been
     * submitted).
     *
     * Note that unapproved holiday leave for the past is deemed to be "dead".
     *
     * @param employee the employee whose holiday summary is to be reassessed.
     * @return a boolean indicating that a change has been made (or not)
     */
    public boolean reassessSummary(EntityManager entityManager, Employee employee) {

        WorkingSummary patternSummary =
                employee.getWorkingPatternsMaster();

        double oldLeaveGranted = patternSummary.getAnnualLeaveGranted();
        double oldLeaveTaken = patternSummary.getAnnualLeaveTaken();
        double oldLeaveRemaining = patternSummary.getAnnualLeaveRemaining();

        double oldLeaveApproved = patternSummary.getAnnualLeaveApproved();
        double oldLeaveNotYetApproved = patternSummary.getAnnualLeaveUnapproved();

        double leaveGranted = 0;
        double leaveTaken = 0;
        double leaveRemaining = 0;

        double leaveApproved = 0;
        double leaveNotYetApproved = 0;

        // Set up details of our employee and the period for our calculations

        Date startOfyear = DaisyChainUtilities.firstDayOfSystemYear(null).getTime();
        Date endOfYear = DaisyChainUtilities.lastDayOfSystemYear(null).getTime();

        periodCalcs.refreshData(entityManager, employee,
                startOfyear, endOfYear);

        periodCalcs.reassessWorkingDays(entityManager);

        Date today = DaisyChainSystem.getToday().getTime();
        
        for (DailyAllocation day : periodCalcs.getDailyInformation()) {

            // Skip any days that are beyond the periphery of our date range
            if (day.getDate().before(startOfyear) || day.getDate().after(endOfYear)) {
                continue;
            }

            leaveGranted += day.getHoliday().getAdjustment();

            if (day.getDate().after(today)) {

                // If in the future, then we are racking up
                // approved and non-approved holiday totals

                leaveApproved += day.getHoliday().getSpend();
                leaveNotYetApproved += day.getUnapprovedHoliday().getSpend();
                
            } else {

                // If in the past then we only rack up leave that has been
                // approved (unapproved past leave is just ignored)

                leaveTaken += day.getHoliday().getSpend();
            }
        }

        leaveRemaining = leaveGranted - leaveTaken - leaveApproved - leaveNotYetApproved;

        // Only update the details if they've changed (the JPA stuff may well
        // perform unexpected/unnecessary processing under the covers)

        if (leaveApproved != oldLeaveApproved ||
                leaveGranted != oldLeaveGranted ||
                leaveRemaining != oldLeaveRemaining ||
                leaveTaken != oldLeaveTaken ||
                leaveNotYetApproved != oldLeaveNotYetApproved) {

            patternSummary.setAnnualLeaveApproved(leaveApproved);
            patternSummary.setAnnualLeaveGranted(leaveGranted);
            patternSummary.setAnnualLeaveRemaining(leaveRemaining);
            patternSummary.setAnnualLeaveTaken(leaveTaken);
            patternSummary.setAnnualLeaveUnapproved(leaveNotYetApproved);

            entityManager.merge(patternSummary);
            
            return true;

        } else {
            return false;
        }
    }

    /**
     * Reassess the holiday status of all employees; it is envisaged that this
     * method will be called as part of end-of-day housekeeping.  Any exceptions
     * are passed back up to the calling method to be reported (although the
     * transaction is also rolled back).  The transaction is managed outside of
     * this method.
     *
     * @param  entityManager the entityManager via which database changes are made
     */
    public void reassessSummaries(EntityManager entityManager) {

        List<Employee> employees = Employee.getEmployees(entityManager);

        for (Employee employee : employees) {

            // Persist any changes
            if (reassessSummary(entityManager, employee)) {
                entityManager.merge(employee.getWorkingPatternsMaster());
            }
        }
    }

    /**
     * Carries out month end processing for holiday requests; this involves
     * identifying records that can be closed (i.e. ones that are in an
     * approved state with a last day prior to the cut-off date).  These
     * holiday requests are given a final refresh before the status is set to
     * "processed" and merged back to the database.
     *
     * @param entityManager manages database updates
     * @param cutOffDate the date up to which holiday requests are included
     */
    public void monthEndProcessing(EntityManager entityManager, Date cutOffDate) {

        // Deal with holiday requests first

        Query query = entityManager.createNamedQuery("HolidayRequest.findAll");
        List<HolidayRequest> requests = query.getResultList();

        for (HolidayRequest request : requests) {

            // We're only interested in approved entires
            if (request.getStatus() != HolidayRequestStatus.APPROVED) {
                continue;
            }

            // Now check to see if the holiday request can be closed off;
            // that can only happen if the holiday was completed on or before
            // the cut-off date

            if (request.getLastDayOfHoliday() != null &&
                    request.getLastDayOfHoliday().compareTo(cutOffDate) > 0) {
                continue;
            }

            // Reassess the holiday for one last time and mark it as processed
            request.setHoursSpent(getHolidaySpend(entityManager, request, null));
            request.setStatus(HolidayRequestStatus.PROCESSED);

            // Merge changes to database
            entityManager.merge(request);
        }

        // Now deal with holiday grants

        query = entityManager.createNamedQuery("HolidayAdjustment.findAll");
        List<HolidayAdjustment> adjustments = query.getResultList();

        for (HolidayAdjustment adjustment : adjustments) {

            // Only interested in released adjustments

            if (adjustment.getStatus() != HolidayAdjustmentStatus.RELEASED) {
                continue;
            }

            // Now check to see if the holiday adjustment can be closed off;
            // that can only happen if the datestamp on the grant was before
            // the cut-off date

            if (adjustment.getAdjustmentDate() != null &&
                    adjustment.getAdjustmentDate().compareTo(cutOffDate) > 0) {
                continue;
            }

            // Mark it as having been processed (can't be altered from now on)
            adjustment.setStatus(HolidayAdjustmentStatus.PROCESSED);

            // Merge changes to database
            entityManager.merge(adjustment);
        }
    }
}